#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# @Time    : 2021/7/27 17:34
# @Author  : Samge
import datetime
import time

import scrapy
from scrapy import signals
from scrapy.exceptions import DontCloseSpider
from scrapy_splash import SplashRequest

from itkz.resources.utils import common_util, settings_util, splash_util
from itkz.resources.utils.log import Log
from itkz.spider_ext import settings


class BaseCommonSpider(scrapy.Spider):
    """
    普通【分页爬取】、【双层循环爬取】基类，支持post & get

    如果是双层循环（一般是关键词&页码双循环），则需要实现方法
    @see BaseCredit#get_double_cycle_max_page(self)
    @see BaseCredit#get_double_cycle_key(self)
    """
    name = None

    # 当前记录值
    curr_record_count = 0
    # 是否重新开始抓
    IS_RESTART = False
    # 是否在完成循环任务后自动关闭爬虫，默认true
    IS_AUTO_CLOSE = True
    # 默认的抓取起始页
    DEFAULT_START_PAGE = 0

    # 统一的日志打印对象
    log = None

    custom_settings = settings_util.get_custom_settings()

    # 列表数据请求地址
    url_list = None

    # 分页请求每页取的数量
    LIMIT = 10000
    # 每一次循环取多少页
    BATCH = 2

    # 第几次循环，默认为1【累加数】
    round = 1
    # 每一次循环的最大页码【累加数】
    end_page = BATCH
    # 最大页数，这是初始值（子类可配置）
    max_page = 29

    # 是否post请求
    IS_POST = False

    # 双循环缓存key下标
    double_cycle_page = 0
    # 是否强制退出当前循环，允许进入下一级循环
    force_exit_loop = False

    # 子类方法是否已经初始化
    has_init_child = False

    # 日志队列
    log_queue = None

    # 是否使用splash
    IS_USE_SPLASH = False

    def __init__(self, name=None, **kwargs):
        
        self.log = Log().getlog()
        self.log_info(f'爬虫初始化  {name} {kwargs}')

        # 读取外部传入的自定义爬虫属性配置
        self.log_queue = kwargs.get('log_queue')
        spider_data = kwargs.get('spider_data') or {}
        for key, value in spider_data.items():
            self.__setattr__(key, value)

        # 初始化子类的一些配置
        if self.has_init_child is False:
            self.init_child(kwargs)
            self.has_init_child = True

        # 复显上次爬取的位置（这里在pyqt5中执行任务，不记录到数据库，所以都是从0开始）
        self.start_page = self.DEFAULT_START_PAGE
        if self.IS_RESTART:
            self.start_page = self.DEFAULT_START_PAGE
        self.log_info('从第 {} 页开始爬取'.format(self.start_page))
        self.end_page = self.get_page_count()

        # 复显外层循环起始位置（这里在pyqt5中执行任务，不记录到数据库，所以都是从0开始）
        if self.get_double_cycle_key():
            self.double_cycle_page = 0
            if self.IS_RESTART:
                self.double_cycle_page = 0

    def get_url_with_check(self, url):
        """
        检查是否使用splash进行请求
        :param url:
        :return:
        """
        url = splash_util.add_splash_tag(url) if self.IS_USE_SPLASH else url
        self.send_log(f"处理链接：{url}")
        return url

    def start_requests(self):
        """
        爬虫启动后首次从这里开启请求，
        空闲后调用 self.spider_idle 方法
        :return:
        """
        for page in range(self.start_page, self.end_page):
            url = self.get_url(page)
            if not url:
                self.log_info(f'当前链接为空，跳过：{url}')
                continue

            yield self.get_request(page, url)

    def spider_idle(self):
        """
        爬虫空闲时会触发
        :return:
        """
        self.next_request()
        self.log_info('spider idled.')
        raise DontCloseSpider

    @classmethod
    def from_crawler(cls, crawler, *args, **kwargs):
        cls.custom_settings = settings_util.merge_settings(cls.custom_settings)
        crawler.settings = settings_util.merge_crawler_settings(cls.custom_settings, crawler.settings)
        instance = cls(*args, **kwargs)
        instance._set_crawler(crawler)
        crawler.signals.connect(instance.spider_idle, signal=signals.spider_idle)
        crawler.signals.connect(instance.dispose, signal=signals.spider_closed)
        return instance

    def dispose(self):
        """
        关闭非pipelines管道数据库连接 / 清空数组
        :return:
        """
        temp_dbs = self.get_dbs() or []
        for _db in temp_dbs:
            _db.close_connect() if _db else None

    def get_url(self, page):
        """
        获取实际的请求链接
        【必须】（子类必须实现该方法）
        """
        raise NotImplementedError('{}.get_url callback is not defined'.format(self.__class__.__name__))

    def get_request_mate(self, page):
        """
        获取实际的请求meta
        【可选】（子类根据需要实现该方法）
        """
        return {'curr_page': page}

    def get_post_body(self, page):
        """
        获取实际的post body的请求体
        【可选】（子类根据需要实现该方法）
        """
        return None

    def get_post_formdata(self, page):
        """
        获取实际的post formdata的请求体
        【可选】（子类根据需要实现该方法）
        """
        return None

    def get_loop_sleep_time(self):
        """
        获取一次循环结束后的休眠时间，默认不休眠，单位：秒
        【可选】（子类根据需要实现该方法）
        """
        return 0

    def init_child(self, kwargs):
        """
        给子类的初始化方法
        【可选】（子类根据需要实现该方法）
        """
        pass

    def get_double_cycle_max_page(self):
        """
        获取【双层循环】最大页数
        （如果有双层循环查询，子类需要实现该方法，返回None则自动关闭爬虫）
        """
        return 0

    def get_double_cycle_key(self):
        """
        获取【双层循环】的缓存key
        （如果有双层循环查询，子类需要实现该方法，返回None则自动关闭爬虫）
        """
        return None

    def get_dbs(self):
        """
        获取爬虫类中初始化的数据库，父类中进行统一关闭
        【可选】（子类根据需要实现该方法）
        """
        return None

    def get_request(self, page, url):
        """
        获取请求对象
        :param page:
        :param url:
        :return:
        """
        if self.IS_POST:
            return scrapy.FormRequest(url=url,
                                      meta=self.get_request_mate(page),
                                      headers=self.get_header(),
                                      dont_filter=True,
                                      formdata=self.get_post_formdata(page),
                                      body=self.get_post_body(page),
                                      method='POST',
                                      )
        elif splash_util.is_splash(url):
            return SplashRequest(url=splash_util.remove_splash_tag(url),
                                 meta=self.get_request_mate(page),
                                 headers=self.get_header(),
                                 dont_filter=True,
                                 args=settings.SPLASH_ARGS
                                 )
        else:
            return scrapy.Request(url=url,
                                  meta=self.get_request_mate(page),
                                  headers=self.get_header(),
                                  dont_filter=True
                                  )

    def get_header(self):
        return {}

    def update_curr_count(self, name, count):
        """
        更新当前页码
        :param name:
        :param count:
        :return:
        """
        self.curr_record_count = count
        return count

    def next_request(self):
        """
        当爬虫空闲时调用，处理是否继续爬取
            单层循环：
                if 当前页码 > 最大页码 self.max_page ：
                    if 自动关闭爬虫：
                        关闭爬虫;
                    else:
                        休眠一段时间后重新进行爬虫任务;
                else:
                    记录当前页码，继续爬取;
            双层循环：
                if 当前页码 > 最大页码 self.max_page ：
                    重置内层循环页码为0；
                    退出内层循环，
                    if 外层页码 > 外层最大页码 self.get_double_cycle_max_page():
                        if 自动关闭爬虫：
                            关闭爬虫;
                        else:
                            休眠一段时间后重新进行爬虫任务;
                    else:
                        记录当前页码，继续爬取;
                        继续外层循环;
                else:
                    记录当前页码，继续爬取;

        :return:
        """
        try:
            curr_record_page = self.update_record()

            if curr_record_page > self.max_page or self.force_exit_loop:
                if self.force_exit_loop:
                    self.log_info('强制退出当前循环，允许进入下一级循环')
                    self.force_exit_loop = False

                self.log_info('已爬取到最大页数：{}，重置数据库缓存页数为起始页：{}'.format(self.max_page, self.DEFAULT_START_PAGE))
                self.update_curr_count(self.name, self.DEFAULT_START_PAGE)

                if self.get_double_cycle_key() is None and self.IS_AUTO_CLOSE:
                    self.log_info(settings.CLOSE_SPIDER_TIP)
                    self.crawler.engine.close_spider(self)
                    return

                elif self.double_cycle_page > self.get_double_cycle_max_page():
                    # 处理 外层页码 > 外层最大页码
                    self.double_cycle_page = 0
                    if self.IS_AUTO_CLOSE:
                        self.log_info(f'{settings.CLOSE_SPIDER_TIP}，并重置外层循环的缓存页数为：0')
                        self.crawler.engine.close_spider(self)
                    else:
                        _txt = f'{datetime.datetime.now()} {self.name} 外层循环抓取完成，并重置外层循环的缓存页数为：0。当前任务为无限循环模式，初始化参数后继续爬取\n'
                        self.log_info(_txt)

                        loop_sleep_time = self.get_loop_sleep_time()
                        if loop_sleep_time > 0:
                            self.log_info(f'当前任务设置自动休眠，随机睡眠 {int(loop_sleep_time / 60)} 分钟后再继续')
                            time.sleep(loop_sleep_time)

                        self.__init__()

                else:
                    if self.get_double_cycle_key():
                        self.double_cycle_page += 1
                        self.log_info('更新下一轮的数据库缓存索引为：{}'.format(self.double_cycle_page))
                        self.update_curr_count(self.get_double_cycle_key(), self.double_cycle_page)

                    self.log_info('即将开始下一轮抓取，重置相关参数：round、start_page、end_page')
                    self.round = 1
                    self.start_page = curr_record_page = self.DEFAULT_START_PAGE
                    self.end_page = self.get_page_count()

            for page in range(curr_record_page, self.end_page):
                url = self.get_url(page)
                if not url:
                    self.log_info(f'当前链接为空，跳过：{url}')
                    continue

                req = self.get_request(page, url)
                req.callback = self.parse
                self.crawler.engine.crawl(req, spider=self)

        except Exception as e:
            self.log_error(f'next_request() 出错： {e}')

    def update_record(self):
        """
        更新数据库,这里会调用多次，每次空闲了都会调用这里
        :return 下一轮抓取的起始页
        """
        curr_record_page = self.get_page_count()
        if curr_record_page <= self.max_page:
            self.update_curr_count(self.name, curr_record_page)
            self.round += 1
            self.end_page = self.get_page_count()
        return curr_record_page

    def get_page_count(self):
        """
        根据起始页、循环次数、每次循环取数常量
        返回页码数
        """
        return self.start_page + self.round * self.BATCH

    def check_sleep_time(self):
        """
        判断时间，如果当前时间在 [1:00~8:00)，则随机睡眠
        """
        self.log_info('判断时间，如果当前时间在 [1:00~8:00)，则随机睡眠')
        curr_hour = time.strftime('%H', time.localtime(time.time()))
        if 1 <= int(curr_hour) < 8:
            sleep_hour = 8 - int(curr_hour)
            random_sleep_time = common_util.get_randm_num(sleep_hour * 60, (sleep_hour + 1) * 60)
            self.log_info(
                '当前时间（{}）在 [1:00~8:00)范围，随机睡眠 {} 分钟后再继续'.format(datetime.datetime.now(), random_sleep_time))
            time.sleep(random_sleep_time * 60)

    def print_error_log(self, e, response):
        """
        处理错误日志
        :param e:
        :param response:
        :return:
        """
        self.send_log(f"错误日志：{e}\n{response.url}\n")

    def log_info(self, *v):
        """
        打印信息
        :param v:
        :return:
        """
        v = ' '.join(['%s' % str(s) for s in v])
        self.send_log(v)
        self.log.info(v)

    def log_error(self, *v):
        """
        打印错误
        :param v:
        :return:
        """
        v = ' '.join(['%s' % str(s) for s in v])
        self.send_log(v)
        self.log.error(v)

    def send_log(self, v):
        """
        发送日志到队列中
        :param v:
        :return:
        """
        if self.log_queue:
            self.log_queue.put(v)

